﻿using System;
using System.Security.Principal;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace King.Security {

    /// see http://support.microsoft.com/default.aspx?scid=kb;en-us;Q306158
    public class Impersonator : IDisposable {

        public enum LogonType {
            /// <summary>
            /// This logon type is intended for users who will be interactively using the computer, such as a user being logged on  
            /// by a terminal server, remote shell, or similar process.
            /// This logon type has the additional expense of caching logon information for disconnected operations; 
            /// therefore, it is inappropriate for some client/server applications,
            /// such as a mail server.
            /// </summary>
            Interactive = 2,

            /// <summary>
            /// This logon type is intended for high performance servers to authenticate plaintext passwords.

            /// The LogonUser function does not cache credentials for this logon type.
            /// </summary>
            Network = 3,

            /// <summary>
            /// This logon type is intended for batch servers, where processes may be executing on behalf of a user without 
            /// their direct intervention. This type is also for higher performance servers that process many plaintext
            /// authentication attempts at a time, such as mail or Web servers. 
            /// The LogonUser function does not cache credentials for this logon type.
            /// </summary>
            Batch = 4,

            /// <summary>
            /// Indicates a service-type logon. The account provided must have the service privilege enabled. 
            /// </summary>
            Service = 5,

            /// <summary>
            /// This logon type is for GINA DLLs that log on users who will be interactively using the computer. 
            /// This logon type can generate a unique audit record that shows when the workstation was unlocked. 
            /// </summary>
            Unlock = 7,

            /// <summary>
            /// This logon type preserves the name and password in the authentication package, which allows the server to make 
            /// connections to other network servers while impersonating the client. A server can accept plaintext credentials 
            /// from a client, call LogonUser, verify that the user can access the system across the network, and still 
            /// communicate with other servers.
            /// NOTE: Windows NT:  This value is not supported. 
            /// </summary>
            NetworkClearText = 8,

            /// <summary>
            /// This logon type allows the caller to clone its current token and specify new credentials for outbound connections.
            /// The new logon session has the same local identifier but uses different credentials for other network connections. 
            /// NOTE: This logon type is supported only by the LOGON32_PROVIDER_WINNT50 logon provider.
            /// NOTE: Windows NT:  This value is not supported. 
            /// </summary>
            NewCredentials = 9,
        }
        public enum LogonProvider {
            /// <summary>
            /// Use the standard logon provider for the system. 
            /// The default security provider is negotiate, unless you pass NULL for the domain name and the user name 
            /// is not in UPN format. In this case, the default provider is NTLM. 
            /// NOTE: Windows 2000/NT:   The default security provider is NTLM.
            /// </summary>
            Default = 0,
        }

        private const string AdvApi32Dll = "advapi32.dll";
        private const string Kernel32Dll = "kernel32.dll";

        [DllImport(AdvApi32Dll, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int LogonUser(
            string userName,
            string domain,
            string password,
            LogonType logonType,
            LogonProvider logonProvider,
            out IntPtr token);

        [DllImport(AdvApi32Dll, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int DuplicateToken(
            IntPtr hToken,
            int impersonationLevel,
            out IntPtr duplicateTokenHandle);

        [DllImport(AdvApi32Dll, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool RevertToSelf();

        [DllImport(Kernel32Dll, CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);

        private WindowsImpersonationContext m_context;

        /// <summary>
        /// Constructor. Starts the impersonation with the given credentials.
        /// Please note that the account that instantiates the Impersonator class
        /// needs to have the 'Act as part of operating system' privilege set.
        /// </summary>
        /// <param name="userName">The name of the user to act as.</param>
        /// <param name="domainName">The domain name of the user to act as.</param>
        /// <param name="password">The password of the user to act as.</param>
        public Impersonator(
            string userName,
            string domain,
            string password) {

            var token = default(IntPtr);
            var duplicateToken = default(IntPtr);

            try {
                if (!RevertToSelf()) 
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                
                if (LogonUser(userName, domain, password, LogonType.Interactive, LogonProvider.Default, out token) == 0)
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                
                if (DuplicateToken(token, 2, out duplicateToken) == 0) 
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                
                m_context = new WindowsIdentity(duplicateToken).Impersonate();
            } 
            
            finally {
                if (token != IntPtr.Zero) 
                    CloseHandle(token);

                if (duplicateToken != IntPtr.Zero) 
                    CloseHandle(duplicateToken);
            }
        }

        private void Dispose(bool finalizing) {
            if (!finalizing)
                GC.SuppressFinalize(this);

            if (m_context == null)
                return;

            m_context.Undo();
        }

        public void Dispose() {
            Dispose(finalizing: true);
        }
        ~Impersonator() {
            Dispose(finalizing: true);
        }
    }
}
